﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Security.Authentication;

namespace org.furia.proxy
{
    public delegate void ClientEvent(object sender);

    public partial class Client : Component
    {

        public event ClientEvent Disconnected;

        public Client()
        {
            InitializeComponent();
        }

        public Client(IContainer container)
        {
            container.Add(this);

            InitializeComponent();
        }

        #region Properties
        private bool master;
        public bool Master
        {
            get
            {
                return master;
            }
            set
            {
                master = value;
            }
        }

        private string remoteHost;
        public string RemoteHost
        {
            get
            {
                return remoteHost;
            }
            set
            {
                remoteHost = value;
            }
        }

        private int remotePort;
        public int RemotePort
        {
            get
            {
                return remotePort;
            }
            set
            {
                remotePort = value;
            }
        }

        private X509Certificate certificate;
        public X509Certificate Certificate
        {
            get
            {
                return certificate;
            }
            set
            {
                certificate = value;
            }
        }

        private X509Certificate authority;
        public X509Certificate Authority
        {
            get
            {
                return authority;
            }
            set
            {
                authority = value;
            }
        }

        private int bufferSize;
        public int BufferSize
        {
            get
            {
                return bufferSize;
            }
            set
            {
                bufferSize = value;
            }
        }
        #endregion

        private Stream stream1 = null;
        private Stream stream2 = null;
        private byte[] buffer1 = null;
        private byte[] buffer2 = null;
        TcpClient client1 = null;
        TcpClient client2 = null;

        public void Start(TcpClient client)
        {
            try
            {
                buffer1 = new byte[bufferSize];
                buffer2 = new byte[bufferSize];
                client1 = client;
                client2 = new TcpClient(remoteHost, remotePort);
                if (master)
                {
                    stream1 = new SslStream(client1.GetStream(), false, new RemoteCertificateValidationCallback(ValidateRemoteCertificate));
                    (stream1 as SslStream).AuthenticateAsServer(certificate, true, SslProtocols.Default, false);
                    if (!(stream1 as SslStream).IsMutuallyAuthenticated)
                    {
                        throw new AuthenticationException();
                    }

                    stream2 = client2.GetStream();
                }
                else
                {
                    stream1 = client1.GetStream();

                    stream2 = new SslStream(client2.GetStream(), false, new RemoteCertificateValidationCallback(ValidateRemoteCertificate), new LocalCertificateSelectionCallback(SelectLocalCertificate));
                    (stream2 as SslStream).AuthenticateAsClient(string.Empty, new X509CertificateCollection(new X509Certificate[] { certificate }), System.Security.Authentication.SslProtocols.Default, false);
                    if (!(stream2 as SslStream).IsMutuallyAuthenticated)
                    {
                        throw new AuthenticationException();
                    }
                }

                DoRead(stream1);
                DoRead(stream2);
            }
            catch
            {
                Stop();
            }
        }

        private void DoRead(Stream stream)
        {
            byte[] buffer = stream == stream1 ? buffer1 : buffer2;
            try
            {
                stream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(OnRead), stream);
            }
            catch
            {
                Stop();
            }
        }

        private void OnRead(IAsyncResult result)
        {
            try
            {
                Stream stream = (Stream)result.AsyncState;
                int read = stream.EndRead(result);
                byte[] buffer = stream == stream1 ? buffer1 : buffer2;
                Stream remote = stream == stream1 ? stream2 : stream1;
                remote.Write(buffer, 0, read);
                remote.Flush();
                DoRead(stream);
            }
            catch
            {
                Stop();
            }
        }

        public void Stop()
        {
            try
            {
                stream1.Close();
            }
            catch { }
            try
            {
                stream2.Close();
            }
            catch { }
            try
            {
                client1.Close();
            }
            catch { }
            try
            {
                client2.Close();
            }
            catch { }
            stream1 = null;
            stream2 = null;
            client1 = null;
            client2 = null;
            buffer1 = null;
            buffer2 = null;
            if (Disconnected != null)
            {
                Disconnected(this);
            }
        }

        private bool ValidateRemoteCertificate(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
        {
            if (errors == SslPolicyErrors.None || errors == SslPolicyErrors.RemoteCertificateNameMismatch)
            {
                foreach (X509ChainElement element in chain.ChainElements)
                {
                    if (element.Certificate.GetRawCertDataString() == authority.GetRawCertDataString())
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        public static X509Certificate SelectLocalCertificate(Object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers)
        {
            return localCertificates[0];
        }

    }
}
